package ngat.oss.simulation;

import java.util.*;
import java.io.*;
import java.text.*;

import ngat.phase2.*;
import ngat.util.*;
import ngat.util.logging.*;
import ngat.astrometry.*;
import ngat.oss.*;

import jyd.storable.*;
import jyd.collection.*;

/** Performs a simulation over one or more nights using a cached ODB for a specified site.*/
public class ScheduleSimulator implements TimeSignalListener {

    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static SimpleTimeZone UTC = new SimpleTimeZone(0, "UTC");

    static {
	sdf.setTimeZone(UTC);
    }

    /** Stores the Proposals.*/
    List cache;

    /** Provides a simulated timestamp for logging.*/    
    TimeProvider timing;

    /** Site latitude/long.*/
    Site site;

    /** For logging.*/
    Logger slog;

    /** Proxy.*/
    LogProxy logger;

    /** Proposal to hold test groups.*/
    Proposal simProposal;

    /** A semaphore set by Time Signal Generator.*/
    BooleanLock lock;

    /** The time.*/
    long time;

    /** Schedule despatcher.*/
    Despatcher despatcher;

    ExecutionTimingModel execModel;

  //   /** Rank sequencer.*/
//     RankSequencer rankSequencer;

  //   /** Selection model.*/
//     SelectionHeuristic  selector;

    /** Environment prediction.*/
    EnvironmentPredictor envPredictor;

    /** Weather prediction.*/
    WeatherModel weatherPredictor;

    /** Create a ScheduleSimulator for the specified site.
     * @param site The telescope site information.
     * @param rankSequencer Sequencer, incorporates scoring model.
     * @param execModel Execution timing and feasibility model.
     * @param selector Selection heuristic.
     * @param envPredictor Environment (seeing/extinction) prediction.
     * @param weatherPredictor Weather prediction.
     */
    public ScheduleSimulator(Site                 site, 
			     Despatcher           despatcher,
			     ExecutionTimingModel execModel,
			     EnvironmentPredictor envPredictor,
			     WeatherModel         weatherPredictor) {
	
        this.site             = site;
	this.despatcher       = despatcher;
	this.execModel        = execModel;
	this.envPredictor     = envPredictor;
	this.weatherPredictor = weatherPredictor;

        timing = new DefaultMutableTimeProvider(0L);

	lock = new BooleanLock(false);

	slog = LogManager.getLogger("SIM");
	logger = new LogProxy("SIMF", "", slog);

    }

 
    /** Add a simulation proposal to the cache.*/
    public void addSimulationProposal(Proposal proposal) {
	cache.add(proposal);
	simProposal = proposal;
    }

    /** Add a group to the simulation proposal.*/
    public void addGroup(Group group) {
	simProposal.addGroup(group);
    }


    /** Start a simulation run
     * @param tsg   The TimeSignalGenerator to post wait requests to.
     * @param start simulation start time.
     * @param end   simulation end time.
     * @param sel   A SimulationEventListener to recieve progress events.
     */
    public void runSimulation(TimeSignalGenerator tsg, 
			      long start,
			      long end,
			      SimulationEventListener sel) {

	double sunelev = 0.0;
	int ig = 0; 
	

	time = start;
	// run over sim period
	while (time < end) {

	    // If its daytime we should jump to next sunset...	 
	    
	    Position sun = Astrometry.getSolarPosition(time);
	    if (sun.getAltitude(time, site) > Math.toRadians(-1.0)) {
		
		// Calculate sunset time nearest to current sim time.	       
		// Use -1 degs to make sure the bugger has really set 
		// as we haven't considered site elevation or refraction.
		long t = time;
		long sunset = t;
		while (sun.getAltitude(t, site) > Math.toRadians(-1.0)) {
		    logger.method("runsim()").log(3, sdf.format(new Date(t))+"** IS DAYTIME");	    
		    t += 5*60*1000L; // +5 minute
		    sun = Astrometry.getSolarPosition(t); 
		    sunset = t;
		}

		// all done, exit		
		if (sunset > end) 
		    break;

		// Wait for sunset...
		synchronized (lock) {
		    tsg.awaitTimingSignal((TimeSignalListener)this, sunset);
		    logger.method("runsim()").log(1,  "** Awaiting Sunset at "+site.getName()+" at "+sdf.format(new Date(sunset)));
		    // DOES NOTHING ! waitSignal();
		    logger.log(1,  "** It is now Sunset at "+site.getName());
		}
	    }
	
	    // 
	    sun = Astrometry.getSolarPosition(time);
	    sunelev = sun.getAltitude(time, site);

	    // check the weather
	    WeatherSnapshot weather = weatherPredictor.predictWeather(time);
	    if (weather.good) {

		// estimate the seeing at that time - hopefully the SimControllerApp will have set this...
	
		EnvironmentSnapshot env = envPredictor.predictEnvironment(time);
	
		Metric best = null;
		try {
		    //best = despatcher.getBestGroupMetric(time, env);
		    // it would be nice if we could get the ranked list here for inspection
		    // should despatcher return this itself ?
		    // e.g. RankedMetrics rankedList = despatcher.getRankedSelection(t,e);
		    //      best = rankedList.bestGroup();
		    
// 		    logger.method("runsim()").log(2, "Start sweep at: "+ScheduleSimulator.sdf.format(new Date(time)));
		    
// 		    // returns the ranked sequence - pass this to the selector 
// 		    List candidates = rankSequencer.getCandidateMetrics(time, env);
		    
// 		    logger.method("runsim()").log(1, ScheduleSimulator.sdf.format(new Date(time))+" Contention= "+candidates.size());
		    
// 		    // notify listener
// 		    sel.contentionResults(candidates.size());
		    
		    // could be null	
		    best = despatcher.getScheduleItem(time, env);

		} catch (Exception e) {
		    e.printStackTrace();
		}
		
		if (best == null) {
		    // Sleep 5 minutes if there's nothing to do...
		    synchronized (lock) {
			tsg.awaitTimingSignal((TimeSignalListener)this, time + 5*60*1000L); // sleep for 5M
			logger.method("runsim()").log(1, "ScheduleSimulator::"+site.getName()+
						      " ** No groups available at "+sdf.format(new Date(time)));
			// DOES NOTHING waitSignal();
		    }
		} else {
		    
		    Group group = best.group;
		    
		    long exec = execModel.getExecTime(group);
		    logger.method("runsim()").
			log(1, " Picking group ["+site.getName()+ig+"] "+
			    group.getClass().getName()+"/"+group.getName()+
			    " P="+Group.toPriorityString(group.getPriority())+
			    " at: "+sdf.format(new Date(time))+
			    " Duration "+(exec/60000)+"M Current Sun elev = "+Position.toDegrees(sunelev,3));
		    ig++;
		    
		    logger.method("runsim()").log(1, "Selected Group estimated completion at "+sdf.format(new Date(time+exec)));
		    
		    // let the caller know we've selected a group and want it executed,
		    // it can do the hist model update, accounting etc...
		    // and update the time model via timeSignal()		
		    sel.groupSelected(best);
		    
		    //		logger.method("runsim()").log(1, " Post timing wait request");
		    //synchronized (lock) {
		    // NOTE Use the stochastic exec time for actual execution
		    //  tsg.awaitTimingSignal((TimeSignalListener)this, time + sexec);
		    
		    //  logger.method("runsim()").log(1, "Waiting for TSG notification...");
		    //waitSignal();
		    ///updateHistory(group, time); the caller will do this now ...hopefully
		    //}
		    
		    // Wait for executor to do the group - it will work out the actual exec and we get notified?
		}

	    } else {
		// Sleep 5 minutes if there's nothing to do...
		synchronized (lock) {
		    tsg.awaitTimingSignal((TimeSignalListener)this, time + 5*60*1000L); // sleep for 5M
		    logger.method("runsim()").log(1, "ScheduleSimulator::"+site.getName()+
						  " ** Bad weather at "+sdf.format(new Date(time)));
			// DOES NOTHING waitSignal();
		    
		}
		
	    } 
	    
	} // next time step
	
	// end of simulation
	sel.simulationCompleted();
	     
    }

   

    /** Wait on timing semaphore (blocking).*/
    public void waitSignal() {
	//lock.setValue(false);
	//try {
	// lock.waitUntilTrue(0); // wait forever...
	//} catch (InterruptedException ix) {}
    }

    /** Returns the site.*/
    public Site getSite() { return site; }

    /** Handle timing signal, releases semaphore.*/
    public void timingSignal(long time) {
	//synchronized (lock) {
	    this.time = time;
	    logger.method("tsr()").log(1,  "Timing signal received ["+sdf.format(new Date(time))+"]");
	    //lock.setValue(true);
	    //}
    }

} // [ScheduleSimulator]    
